// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Reflection.Internal;
using System.Runtime.InteropServices;

namespace System.Reflection.Metadata
{
    // TODO: argument checking
    public struct BlobWriter
    {
        // writable slice:
        private readonly byte[] _buffer;
        private readonly int _start;
        private readonly int _end;  // exclusive

        // position in buffer relative to the beginning of the array:
        private int _position;

        public BlobWriter(int size)
            : this(new byte[size])
        {
        }

        public BlobWriter(byte[] buffer)
            : this(buffer, 0, buffer.Length)
        {
        }

        public BlobWriter(Blob blob)
            : this(blob.Buffer, blob.Start, blob.Length)
        {
        }

        public BlobWriter(byte[] buffer, int start, int count)
        {
            Debug.Assert(buffer != null);
            Debug.Assert(count >= 0);
            Debug.Assert(count <= buffer.Length - start);

            _buffer = buffer;
            _start = start;
            _position = start;
            _end = start + count;
        }

        internal bool IsDefault => _buffer == null;

        /// <summary>
        /// Compares the current content of this writer with another one.
        /// </summary>
        public bool ContentEquals(BlobWriter other)
        {
            return Length == other.Length && _buffer.AsSpan(_start, Length).SequenceEqual(other._buffer.AsSpan(other._start, other.Length));
        }

        public int Offset
        {
            get
            {
                return _position - _start;
            }
            set
            {
                if (value < 0 || _start > _end - value)
                {
                    Throw.ValueArgumentOutOfRange();
                }

                _position = _start + value;
            }
        }

        public int Length => _end - _start;
        public int RemainingBytes => _end - _position;
        public Blob Blob => new Blob(_buffer, _start, Length);

        public byte[] ToArray()
        {
            return ToArray(0, Offset);
        }

        /// <exception cref="ArgumentOutOfRangeException">Range specified by <paramref name="start"/> and <paramref name="byteCount"/> falls outside of the bounds of the buffer content.</exception>
        public byte[] ToArray(int start, int byteCount)
        {
            BlobUtilities.ValidateRange(Length, start, byteCount, nameof(byteCount));

            return _buffer.AsSpan(_start + start, byteCount).ToArray();
        }

        public ImmutableArray<byte> ToImmutableArray()
        {
            return ToImmutableArray(0, Offset);
        }

        /// <exception cref="ArgumentOutOfRangeException">Range specified by <paramref name="start"/> and <paramref name="byteCount"/> falls outside of the bounds of the buffer content.</exception>
        public ImmutableArray<byte> ToImmutableArray(int start, int byteCount)
        {
            BlobUtilities.ValidateRange(Length, start, byteCount, nameof(byteCount));

            return ImmutableArray.Create(_buffer.AsSpan(_start + start, byteCount));
        }

        private int Advance(int value)
        {
            Debug.Assert(value >= 0);

            int position = _position;
            if (position > _end - value)
            {
                Throw.OutOfBounds();
            }

            _position = position + value;
            return position;
        }

        /// <exception cref="ArgumentOutOfRangeException"><paramref name="byteCount"/> is negative.</exception>
        public void WriteBytes(byte value, int byteCount)
        {
            if (byteCount < 0)
            {
                Throw.ArgumentOutOfRange(nameof(byteCount));
            }

            int start = Advance(byteCount);
            _buffer.AsSpan(start, byteCount).Fill(value);
        }

        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="byteCount"/> is negative.</exception>
        public unsafe void WriteBytes(byte* buffer, int byteCount)
        {
            if (buffer is null)
            {
                Throw.ArgumentNull(nameof(buffer));
            }

            if (byteCount < 0)
            {
                Throw.ArgumentOutOfRange(nameof(byteCount));
            }

            WriteBytes(new ReadOnlySpan<byte>(buffer, byteCount));
        }

        internal void WriteBytes(ReadOnlySpan<byte> buffer)
        {
            int start = Advance(buffer.Length);
            buffer.CopyTo(_buffer.AsSpan(start));
        }

        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        public void WriteBytes(BlobBuilder source)
        {
            if (source is null)
            {
                Throw.ArgumentNull(nameof(source));
            }

            source.WriteContentTo(ref this);
        }

        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="byteCount"/> is negative.</exception>
        public int WriteBytes(Stream source, int byteCount)
        {
            if (source is null)
            {
                Throw.ArgumentNull(nameof(source));
            }

            if (byteCount < 0)
            {
                Throw.ArgumentOutOfRange(nameof(byteCount));
            }

            int start = Advance(byteCount);
            int bytesRead = source.TryReadAll(_buffer, start, byteCount);
            _position = start + bytesRead;
            return bytesRead;
        }

        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
        public void WriteBytes(ImmutableArray<byte> buffer)
        {
            if (buffer.IsDefault)
            {
                Throw.ArgumentNull(nameof(buffer));
            }

            WriteBytes(buffer.AsSpan());
        }

        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Range specified by <paramref name="start"/> and <paramref name="byteCount"/> falls outside of the bounds of the <paramref name="buffer"/>.</exception>
        public void WriteBytes(ImmutableArray<byte> buffer, int start, int byteCount)
        {
            if (buffer.IsDefault)
            {
                Throw.ArgumentNull(nameof(buffer));
            }

            BlobUtilities.ValidateRange(buffer.Length, start, byteCount, nameof(byteCount));

            WriteBytes(buffer.AsSpan(start, byteCount));
        }

        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
        public void WriteBytes(byte[] buffer)
        {
            if (buffer is null)
            {
                Throw.ArgumentNull(nameof(buffer));
            }

            WriteBytes(buffer.AsSpan());
        }

        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Range specified by <paramref name="start"/> and <paramref name="byteCount"/> falls outside of the bounds of the <paramref name="buffer"/>.</exception>
        public void WriteBytes(byte[] buffer, int start, int byteCount)
        {
            if (buffer is null)
            {
                Throw.ArgumentNull(nameof(buffer));
            }

            BlobUtilities.ValidateRange(buffer.Length, start, byteCount, nameof(byteCount));

            WriteBytes(buffer.AsSpan(start, byteCount));
        }

        public void PadTo(int offset)
        {
            WriteBytes(0, offset - Offset);
        }

        public void Align(int alignment)
        {
            int offset = Offset;
            WriteBytes(0, BitArithmetic.Align(offset, alignment) - offset);
        }

        public void WriteBoolean(bool value)
        {
            WriteByte((byte)(value ? 1 : 0));
        }

        public void WriteByte(byte value)
        {
            int start = Advance(sizeof(byte));
            _buffer[start] = value;
        }

        public void WriteSByte(sbyte value)
        {
            WriteByte(unchecked((byte)value));
        }

        public void WriteDouble(double value)
        {
            int start = Advance(sizeof(double));
            _buffer.WriteDouble(start, value);
        }

        public void WriteSingle(float value)
        {
            int start = Advance(sizeof(float));
            _buffer.WriteSingle(start, value);
        }

        public void WriteInt16(short value)
        {
            WriteUInt16(unchecked((ushort)value));
        }

        public void WriteUInt16(ushort value)
        {
            int start = Advance(sizeof(ushort));
            _buffer.WriteUInt16(start, value);
        }

        public void WriteInt16BE(short value)
        {
            WriteUInt16BE(unchecked((ushort)value));
        }

        public void WriteUInt16BE(ushort value)
        {
            int start = Advance(sizeof(ushort));
            _buffer.WriteUInt16BE(start, value);
        }

        public void WriteInt32BE(int value)
        {
            WriteUInt32BE(unchecked((uint)value));
        }

        public void WriteUInt32BE(uint value)
        {
            int start = Advance(sizeof(uint));
            _buffer.WriteUInt32BE(start, value);
        }

        public void WriteInt32(int value)
        {
            WriteUInt32(unchecked((uint)value));
        }

        public void WriteUInt32(uint value)
        {
            int start = Advance(sizeof(uint));
            _buffer.WriteUInt32(start, value);
        }

        public void WriteInt64(long value)
        {
            WriteUInt64(unchecked((ulong)value));
        }

        public void WriteUInt64(ulong value)
        {
            int start = Advance(sizeof(ulong));
            _buffer.WriteUInt64(start, value);
        }

        public void WriteDecimal(decimal value)
        {
            int start = Advance(BlobUtilities.SizeOfSerializedDecimal);
            _buffer.WriteDecimal(start, value);
        }

        public void WriteGuid(Guid value)
        {
            int start = Advance(BlobUtilities.SizeOfGuid);
            _buffer.WriteGuid(start, value);
        }

        public void WriteDateTime(DateTime value)
        {
            WriteInt64(value.Ticks);
        }

        /// <summary>
        /// Writes a reference to a heap (heap offset) or a table (row number).
        /// </summary>
        /// <param name="reference">Heap offset or table row number.</param>
        /// <param name="isSmall">True to encode the reference as 16-bit integer, false to encode as 32-bit integer.</param>
        public void WriteReference(int reference, bool isSmall)
        {
            // This code is a very hot path, hence we don't check if the reference actually fits 2B.

            if (isSmall)
            {
                Debug.Assert(unchecked((ushort)reference) == reference);
                WriteUInt16((ushort)reference);
            }
            else
            {
                WriteInt32(reference);
            }
        }

        /// <summary>
        /// Writes UTF-16 (little-endian) encoded string at the current position.
        /// </summary>
        /// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
        public void WriteUTF16(char[] value)
        {
            if (value is null)
            {
                Throw.ArgumentNull(nameof(value));
            }

            WriteUTF16(value.AsSpan());
        }

        /// <summary>
        /// Writes UTF-16 (little-endian) encoded string at the current position.
        /// </summary>
        /// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
        public void WriteUTF16(string value)
        {
            if (value is null)
            {
                Throw.ArgumentNull(nameof(value));
            }

            WriteUTF16(value.AsSpan());
        }

        private void WriteUTF16(ReadOnlySpan<char> value)
        {
            if (BitConverter.IsLittleEndian)
            {
                WriteBytes(MemoryMarshal.AsBytes(value));
            }
            else
            {
                foreach (char c in value)
                {
                    WriteUInt16(c);
                }
            }
        }

        /// <summary>
        /// Writes string in SerString format (see ECMA-335-II 23.3 Custom attributes).
        /// </summary>
        /// <remarks>
        /// The string is UTF-8 encoded and prefixed by the its size in bytes.
        /// Null string is represented as a single byte 0xFF.
        /// </remarks>
        /// <exception cref="InvalidOperationException">Builder is not writable, it has been linked with another one.</exception>
        public void WriteSerializedString(string? str)
        {
            if (str == null)
            {
                WriteByte(0xff);
                return;
            }

            WriteUTF8(str, 0, str.Length, allowUnpairedSurrogates: true, prependSize: true);
        }

        /// <summary>
        /// Writes string in User String (#US) heap format (see ECMA-335-II 24.2.4 #US and #Blob heaps):
        /// </summary>
        /// <remarks>
        /// The string is UTF-16 encoded and prefixed by the its size in bytes.
        ///
        /// This final byte holds the value 1 if and only if any UTF-16 character within the string has any bit set in its top byte,
        /// or its low byte is any of the following: 0x01-0x08, 0x0E-0x1F, 0x27, 0x2D, 0x7F. Otherwise, it holds 0.
        /// The 1 signifies Unicode characters that require handling beyond that normally provided for 8-bit encoding sets.
        /// </remarks>
        /// <exception cref="InvalidOperationException">Builder is not writable, it has been linked with another one.</exception>
        public void WriteUserString(string value)
        {
            if (value is null)
            {
                Throw.ArgumentNull(nameof(value));
            }

            WriteCompressedInteger(BlobUtilities.GetUserStringByteLength(value.Length));
            WriteUTF16(value);
            WriteByte(BlobUtilities.GetUserStringTrailingByte(value));
        }

        /// <summary>
        /// Writes UTF-8 encoded string at the current position.
        /// </summary>
        /// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
        public void WriteUTF8(string value, bool allowUnpairedSurrogates)
        {
            if (value is null)
            {
                Throw.ArgumentNull(nameof(value));
            }

            WriteUTF8(value, 0, value.Length, allowUnpairedSurrogates, prependSize: false);
        }

        private unsafe void WriteUTF8(string str, int start, int length, bool allowUnpairedSurrogates, bool prependSize)
        {
            fixed (char* strPtr = str)
            {
                char* charPtr = strPtr + start;
                int byteCount = BlobUtilities.GetUTF8ByteCount(charPtr, length);

                if (prependSize)
                {
                    WriteCompressedInteger(byteCount);
                }

                int startOffset = Advance(byteCount);
                _buffer.WriteUTF8(startOffset, charPtr, length, byteCount, allowUnpairedSurrogates);
            }
        }

        /// <summary>
        /// Implements compressed signed integer encoding as defined by ECMA-335-II chapter 23.2: Blobs and signatures.
        /// </summary>
        /// <remarks>
        /// If the value lies between -64 (0xFFFFFFC0) and 63 (0x3F), inclusive, encode as a one-byte integer:
        /// bit 7 clear, value bits 5 through 0 held in bits 6 through 1, sign bit (value bit 31) in bit 0.
        ///
        /// If the value lies between -8192 (0xFFFFE000) and 8191 (0x1FFF), inclusive, encode as a two-byte integer:
        /// 15 set, bit 14 clear, value bits 12 through 0 held in bits 13 through 1, sign bit(value bit 31) in bit 0.
        ///
        /// If the value lies between -268435456 (0xF000000) and 268435455 (0x0FFFFFFF), inclusive, encode as a four-byte integer:
        /// 31 set, 30 set, bit 29 clear, value bits 27 through 0 held in bits 28 through 1, sign bit(value bit 31) in bit 0.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> can't be represented as a compressed signed integer.</exception>
        public void WriteCompressedSignedInteger(int value)
        {
            BlobWriterImpl.WriteCompressedSignedInteger(ref this, value);
        }

        /// <summary>
        /// Implements compressed unsigned integer encoding as defined by ECMA-335-II chapter 23.2: Blobs and signatures.
        /// </summary>
        /// <remarks>
        /// If the value lies between 0 (0x00) and 127 (0x7F), inclusive,
        /// encode as a one-byte integer (bit 7 is clear, value held in bits 6 through 0).
        ///
        /// If the value lies between 28 (0x80) and 214 - 1 (0x3FFF), inclusive,
        /// encode as a 2-byte integer with bit 15 set, bit 14 clear(value held in bits 13 through 0).
        ///
        /// Otherwise, encode as a 4-byte integer, with bit 31 set, bit 30 set, bit 29 clear (value held in bits 28 through 0).
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> can't be represented as a compressed unsigned integer.</exception>
        public void WriteCompressedInteger(int value)
        {
            BlobWriterImpl.WriteCompressedInteger(ref this, unchecked((uint)value));
        }

        /// <summary>
        /// Writes a constant value (see ECMA-335 Partition II section 22.9) at the current position.
        /// </summary>
        /// <exception cref="ArgumentException"><paramref name="value"/> is not of a constant type.</exception>
        public void WriteConstant(object? value)
        {
            BlobWriterImpl.WriteConstant(ref this, value);
        }

        public void Clear()
        {
            _position = _start;
        }
    }
}
